iT邦幫忙

0

UML 分析 Android 架構 - KunMinX 的 MVVM 架構框架 (MVVM 架構框架的首選, 7.7k 顆星)

  • 分享至 

  • xImage
  •  

KunMinX - Jetpack-MVVM-Best-Practice

別忘了按下讚並追蹤我喔~

作者的 Blog

官方架構圖

UML MVVM 架構圖

既然是 MVVM 我們就依照 ModelViewViewModel 的 Base 來看。

Model

DataResult

DataResult Code 可以知道它負責處理取得資料的部分,而泛型 T 估計就是 JavaBeanEntityJOPO,藉由 interface Result 可以回傳 T 的資料。

public class DataResult<T> {

    private final T mEntity;
    private final ResponseStatus mResponseStatus;

    public DataResult(T entity, ResponseStatus responseStatus) {
        mEntity = entity;
        mResponseStatus = responseStatus;
    }

    public DataResult(T entity){
        mEntity=entity;
        mResponseStatus=new ResponseStatus();
    }

    public T getResult() {
        return mEntity;
    }

    public ResponseStatus getResponseStatus() {
        return mResponseStatus;
    }

    public interface Result<T> {
        void onResult(DataResult<T> dataResult);
    }
}

ResponseStatus

ResponseStatus 為回應的狀態、資料與來源。

public class ResponseStatus {

    private String responseCode = "";
    private boolean success = true;
    private Enum<ResultSource> source = ResultSource.NETWORK;

    public ResponseStatus() {
    }

    public ResponseStatus(String responseCode, boolean success) {
        this.responseCode = responseCode;
        this.success = success;
    }

    public ResponseStatus(String responseCode, boolean success, Enum<ResultSource> source) {
        this(responseCode, success);
        this.source = source;
    }

    public String getResponseCode() {
        return responseCode;
    }

    public boolean isSuccess() {
        return success;
    }

    public Enum<ResultSource> getSource() {
        return source;
    }
}

ResultSource

ResultSource 為資料的種類。

public enum ResultSource {
    NETWORK, DATABASE, LOCAL_FILE
}

View

View 的 Base 只有簡單的繼承。

BaseActivity

public abstract class BaseActivity extends DataBindingActivity {

    private final ViewModelScope mViewModelScope = new ViewModelScope();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {

        BarUtils.setStatusBarColor(this, Color.TRANSPARENT);
        BarUtils.setStatusBarLightMode(this, true);

        super.onCreate(savedInstanceState);

        getLifecycle().addObserver(NetworkStateManager.getInstance());

    }

    protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull Class<T> modelClass) {
        return mViewModelScope.getActivityScopeViewModel(this, modelClass);
    }

    protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {
        return mViewModelScope.getApplicationScopeViewModel(modelClass);
    }

    @Override
    public Resources getResources() {
        if (ScreenUtils.isPortrait()) {
            return AdaptScreenUtils.adaptWidth(super.getResources(), 360);
        } else {
            return AdaptScreenUtils.adaptHeight(super.getResources(), 640);
        }
    }

    protected void toggleSoftInput() {
        InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE);
        imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);
    }

    protected void openUrlInBrowser(String url) {
        Uri uri = Uri.parse(url);
        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
        startActivity(intent);
    }
}

DataBindingActivity

public abstract class DataBindingActivity extends AppCompatActivity {
    private ViewDataBinding mBinding;
    private TextView mTvStrictModeTip;

    public DataBindingActivity() {
    }

    protected abstract void initViewModel();

    protected abstract DataBindingConfig getDataBindingConfig();

    protected ViewDataBinding getBinding() {
        if (this.isDebug() && this.mBinding != null && this.mTvStrictModeTip == null) {
            this.mTvStrictModeTip = new TextView(this.getApplicationContext());
            this.mTvStrictModeTip.setAlpha(0.4F);
            this.mTvStrictModeTip.setPadding(this.mTvStrictModeTip.getPaddingLeft() + 24, this.mTvStrictModeTip.getPaddingTop() + 64, this.mTvStrictModeTip.getPaddingRight() + 24, this.mTvStrictModeTip.getPaddingBottom() + 24);
            this.mTvStrictModeTip.setGravity(1);
            this.mTvStrictModeTip.setTextSize(10.0F);
            this.mTvStrictModeTip.setBackgroundColor(-1);
            String tip = this.getString(string.debug_databinding_warning, new Object[]{this.getClass().getSimpleName()});
            this.mTvStrictModeTip.setText(tip);
            ((ViewGroup)this.mBinding.getRoot()).addView(this.mTvStrictModeTip);
        }

        return this.mBinding;
    }

    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.initViewModel();
        DataBindingConfig dataBindingConfig = this.getDataBindingConfig();
        ViewDataBinding binding = DataBindingUtil.setContentView(this, dataBindingConfig.getLayout());
        binding.setLifecycleOwner(this);
        binding.setVariable(dataBindingConfig.getVmVariableId(), dataBindingConfig.getStateViewModel());
        SparseArray<Object> bindingParams = dataBindingConfig.getBindingParams();
        int i = 0;

        for(int length = bindingParams.size(); i < length; ++i) {
            binding.setVariable(bindingParams.keyAt(i), bindingParams.valueAt(i));
        }

        this.mBinding = binding;
    }

    public boolean isDebug() {
        return this.getApplicationContext().getApplicationInfo() != null && (this.getApplicationContext().getApplicationInfo().flags & 2) != 0;
    }

    protected void onDestroy() {
        super.onDestroy();
        this.mBinding.unbind();
        this.mBinding = null;
    }
}

BaseFragment

public abstract class BaseFragment extends DataBindingFragment {

    private final ViewModelScope mViewModelScope = new ViewModelScope();

    protected <T extends ViewModel> T getFragmentScopeViewModel(@NonNull Class<T> modelClass) {
        return mViewModelScope.getFragmentScopeViewModel(this, modelClass);
    }

    protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull Class<T> modelClass) {
        return mViewModelScope.getActivityScopeViewModel(mActivity, modelClass);
    }

    protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {
        return mViewModelScope.getApplicationScopeViewModel(modelClass);
    }

    protected NavController nav() {
        return NavHostFragment.findNavController(this);
    }


    protected void toggleSoftInput() {
        InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Activity.INPUT_METHOD_SERVICE);
        imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);
    }

    protected void openUrlInBrowser(String url) {
        Uri uri = Uri.parse(url);
        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
        startActivity(intent);
    }

    protected Context getApplicationContext() {
        return mActivity.getApplicationContext();
    }

}

DataBindingFragment

public abstract class DataBindingFragment extends Fragment {
    protected AppCompatActivity mActivity;
    private ViewDataBinding mBinding;
    private TextView mTvStrictModeTip;

    public DataBindingFragment() {
    }

    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        this.mActivity = (AppCompatActivity)context;
    }

    protected abstract void initViewModel();

    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.initViewModel();
    }

    protected abstract DataBindingConfig getDataBindingConfig();

    protected ViewDataBinding getBinding() {
        if (this.isDebug() && this.mBinding != null && this.mTvStrictModeTip == null) {
            this.mTvStrictModeTip = new TextView(this.getContext());
            this.mTvStrictModeTip.setAlpha(0.5F);
            this.mTvStrictModeTip.setPadding(this.mTvStrictModeTip.getPaddingLeft() + 24, this.mTvStrictModeTip.getPaddingTop() + 64, this.mTvStrictModeTip.getPaddingRight() + 24, this.mTvStrictModeTip.getPaddingBottom() + 24);
            this.mTvStrictModeTip.setGravity(1);
            this.mTvStrictModeTip.setTextSize(10.0F);
            this.mTvStrictModeTip.setBackgroundColor(-1);
            String tip = this.getString(string.debug_databinding_warning, new Object[]{this.getClass().getSimpleName()});
            this.mTvStrictModeTip.setText(tip);
            ((ViewGroup)this.mBinding.getRoot()).addView(this.mTvStrictModeTip);
        }

        return this.mBinding;
    }

    @Nullable
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        DataBindingConfig dataBindingConfig = this.getDataBindingConfig();
        ViewDataBinding binding = DataBindingUtil.inflate(inflater, dataBindingConfig.getLayout(), container, false);
        binding.setLifecycleOwner(this.getViewLifecycleOwner());
        binding.setVariable(dataBindingConfig.getVmVariableId(), dataBindingConfig.getStateViewModel());
        SparseArray<Object> bindingParams = dataBindingConfig.getBindingParams();
        int i = 0;

        for(int length = bindingParams.size(); i < length; ++i) {
            binding.setVariable(bindingParams.keyAt(i), bindingParams.valueAt(i));
        }

        this.mBinding = binding;
        return binding.getRoot();
    }

    public boolean isDebug() {
        return this.mActivity.getApplicationContext().getApplicationInfo() != null && (this.mActivity.getApplicationContext().getApplicationInfo().flags & 2) != 0;
    }

    public void onDestroyView() {
        super.onDestroyView();
        this.mBinding.unbind();
        this.mBinding = null;
    }
}

ViewModel

ViewModelScope

這裡的 ViewModelScope 跟官方的概念不太一樣

Android Architecture Coroutines ViewModelScope

為應用中的每個 ViewModel 定義了ViewModelScope。如果 ViewModel 已清除,則在此範圍內啟動的協程都會自動取消。如果您具有僅在 ViewModel 處於活動狀態時才需要完成的工作,此時協程非常有用。例如,如果要為佈局計算某些數據,則應將工作範圍限定至ViewModel,以便在 ViewModel 清除後,系統會自動取消工作以避免消耗資源。

public class ViewModelScope {
    private ViewModelProvider mFragmentProvider;
    private ViewModelProvider mActivityProvider;
    private ViewModelProvider mApplicationProvider;

    public ViewModelScope() {
    }

    public <T extends ViewModel> T getFragmentScopeViewModel(@NonNull Fragment fragment, @NonNull Class<T> modelClass) {
        if (this.mFragmentProvider == null) {
            this.mFragmentProvider = new ViewModelProvider(fragment);
        }

        return this.mFragmentProvider.get(modelClass);
    }

    public <T extends ViewModel> T getActivityScopeViewModel(@NonNull AppCompatActivity activity, @NonNull Class<T> modelClass) {
        if (this.mActivityProvider == null) {
            this.mActivityProvider = new ViewModelProvider(activity);
        }

        return this.mActivityProvider.get(modelClass);
    }

    public <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {
        if (this.mApplicationProvider == null) {
            this.mApplicationProvider = new ViewModelProvider(ApplicationInstance.getInstance());
        }

        return this.mApplicationProvider.get(modelClass);
    }
}

getApplicationScopeViewModel 方法使用 ApplicationInstance.getInstance() 作為宣告ViewModelProvider 的參數

ApplicationInstance

public class ApplicationInstance implements ViewModelStoreOwner {
    private static final ApplicationInstance sInstance = new ApplicationInstance();
    private ViewModelStore mAppViewModelStore;

    private ApplicationInstance() {
    }

    public static ApplicationInstance getInstance() {
        return sInstance;
    }

    @NonNull
    public ViewModelStore getViewModelStore() {
        if (this.mAppViewModelStore == null) {
            this.mAppViewModelStore = new ViewModelStore();
        }

        return this.mAppViewModelStore;
    }
}


UseCase


如果說看不清邊界(boundaries)可以看下面這張

  • UseCaseScheduler
    定義排程者要做的事情。
    • execute : 執行 UseCase。
    • notifyResponse : 通知回應。
    • onError : 偵測到錯誤
  • UseCaseThreadPoolScheduler
    排程者 : 使用 ThreadPoolExecutor 執行異步任務來做 UseCaseUseCaseCallback
    為什麼使用 ThreadPoolExecutor 而不是 Thread 就好 ?
    因為使用 Thread 有兩個缺點
    1. 每次都會new一個執行緒,執行完後銷燬,不能複用。
    2. 如果系統的併發量剛好比較大,需要大量執行緒,那麼這種每次new的方式會搶資源的。
      ThreadPoolExecutor 的好處是可以做到執行緒複用,並且使用盡量少的執行緒去執行更多的任務,效率和效能都相當不錯。
  • UseCaseHandler
    負責執行 UseCase 的角色。
    提供 execute 方法負責執行 UseCase。並決定執行結果 onSuccess and onError 的 UI 畫面。
  • UseCase
    實作 UseCase 的核心方法。

UseCase

/**
 * Use cases are the entry points to the domain layer.
 *
 * @param <Q> the request type
 * @param <P> the response type
 */
public abstract class UseCase<Q extends UseCase.RequestValues, P extends UseCase.ResponseValue> {

    private Q mRequestValues;

    private UseCaseCallback<P> mUseCaseCallback;

    public Q getRequestValues() {
        return mRequestValues;
    }

    public void setRequestValues(Q requestValues) {
        mRequestValues = requestValues;
    }

    public UseCaseCallback<P> getUseCaseCallback() {
        return mUseCaseCallback;
    }

    public void setUseCaseCallback(UseCaseCallback<P> useCaseCallback) {
        mUseCaseCallback = useCaseCallback;
    }

    void run() {
        executeUseCase(mRequestValues);
    }

    protected abstract void executeUseCase(Q requestValues);

    /**
     * Data passed to a request.
     */
    public interface RequestValues {
    }

    /**
     * Data received from a request.
     */
    public interface ResponseValue {
    }

    public interface UseCaseCallback<R> {
        void onSuccess(R response);

        void onError();
    }
}

UseCaseHandler

提供 execute 方法負責執行 UseCase。並決定執行結果 onSuccess and onError 的 UI 畫面。

/**
 * Runs {@link UseCase}s using a {@link UseCaseScheduler}.
 */
public class UseCaseHandler {

    private static UseCaseHandler INSTANCE;

    private final UseCaseScheduler mUseCaseScheduler;

    public UseCaseHandler(UseCaseScheduler useCaseScheduler) {
        mUseCaseScheduler = useCaseScheduler;
    }

    public static UseCaseHandler getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new UseCaseHandler(new UseCaseThreadPoolScheduler());
        }
        return INSTANCE;
    }

    public <T extends UseCase.RequestValues, R extends UseCase.ResponseValue> void execute(
            final UseCase<T, R> useCase, T values, UseCase.UseCaseCallback<R> callback) {
        useCase.setRequestValues(values);
        //noinspection unchecked
        useCase.setUseCaseCallback(new UiCallbackWrapper(callback, this));

        // The network request might be handled in a different thread so make sure
        // Espresso knows
        // that the app is busy until the response is handled.

        // This callback may be called twice, once for the cache and once for loading
        // the data from the server API, so we check before decrementing, otherwise
        // it throws "Counter has been corrupted!" exception.
        mUseCaseScheduler.execute(useCase::run);
    }

    private <V extends UseCase.ResponseValue> void notifyResponse(final V response,
                                                                  final UseCase.UseCaseCallback<V> useCaseCallback) {
        mUseCaseScheduler.notifyResponse(response, useCaseCallback);
    }

    private <V extends UseCase.ResponseValue> void notifyError(
            final UseCase.UseCaseCallback<V> useCaseCallback) {
        mUseCaseScheduler.onError(useCaseCallback);
    }

    private static final class UiCallbackWrapper<V extends UseCase.ResponseValue> implements
            UseCase.UseCaseCallback<V> {
        private final UseCase.UseCaseCallback<V> mCallback;
        private final UseCaseHandler mUseCaseHandler;

        public UiCallbackWrapper(UseCase.UseCaseCallback<V> callback,
                                 UseCaseHandler useCaseHandler) {
            mCallback = callback;
            mUseCaseHandler = useCaseHandler;
        }

        @Override
        public void onSuccess(V response) {
            mUseCaseHandler.notifyResponse(response, mCallback);
        }

        @Override
        public void onError() {
            mUseCaseHandler.notifyError(mCallback);
        }
    }
}

UseCaseScheduler

定義排程者要做的事情。

  • execute : 執行 UseCase。
  • notifyResponse : 通知回應。
  • onError : 偵測到錯誤
/**
 * Interface for schedulers, see {@link UseCaseThreadPoolScheduler}.
 */
public interface UseCaseScheduler {

    void execute(Runnable runnable);

    <V extends UseCase.ResponseValue> void notifyResponse(final V response,
                                                          final UseCase.UseCaseCallback<V> useCaseCallback);

    <V extends UseCase.ResponseValue> void onError(
            final UseCase.UseCaseCallback<V> useCaseCallback);
}

UseCaseThreadPoolScheduler

排程者 : 使用 ThreadPoolExecutor 執行異步任務來做 UseCaseUseCaseCallback

/**
 * Executes asynchronous tasks using a {@link ThreadPoolExecutor}.
 * <p>
 * See also {@link Executors} for a list of factory methods to create common
 * {@link java.util.concurrent.ExecutorService}s for different scenarios.
 */
public class UseCaseThreadPoolScheduler implements UseCaseScheduler {

    public static final int POOL_SIZE = 2;
    public static final int MAX_POOL_SIZE = 4 * 2;
    public static final int FIXED_POOL_SIZE = 4;
    public static final int TIMEOUT = 30;
    final ThreadPoolExecutor mThreadPoolExecutor;
    private final Handler mHandler = new Handler();

    public UseCaseThreadPoolScheduler() {
        mThreadPoolExecutor = new ThreadPoolExecutor(FIXED_POOL_SIZE, FIXED_POOL_SIZE, TIMEOUT,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>());
    }

    @Override
    public void execute(Runnable runnable) {
        mThreadPoolExecutor.execute(runnable);
    }

    @Override
    public <V extends UseCase.ResponseValue> void notifyResponse(final V response,
                                                                 final UseCase.UseCaseCallback<V> useCaseCallback) {
        mHandler.post(() -> {
            if (null != useCaseCallback) {
                useCaseCallback.onSuccess(response);
            }
        });
    }

    @Override
    public <V extends UseCase.ResponseValue> void onError(
            final UseCase.UseCaseCallback<V> useCaseCallback) {
        mHandler.post(useCaseCallback::onError);
    }

}

Demo

Model

DataRepository

public class DataRepository {

    private static final DataRepository S_REQUEST_MANAGER = new DataRepository();

    private DataRepository() {
    }

    public static DataRepository getInstance() {
        return S_REQUEST_MANAGER;
    }

    private final Retrofit retrofit;

    {
        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient client = new OkHttpClient.Builder()
            .connectTimeout(8, TimeUnit.SECONDS)
            .readTimeout(8, TimeUnit.SECONDS)
            .writeTimeout(8, TimeUnit.SECONDS)
            .addInterceptor(logging)
            .build();
        retrofit = new Retrofit.Builder()
            .baseUrl(APIs.BASE_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build();
    }


    public void getFreeMusic(DataResult.Result<TestAlbum> result) {

        Gson gson = new Gson();
        Type type = new TypeToken<TestAlbum>() {
        }.getType();
        TestAlbum testAlbum = gson.fromJson(Utils.getApp().getString(R.string.free_music_json), type);

        result.onResult(new DataResult<>(testAlbum, new ResponseStatus()));
    }

    public void getLibraryInfo(DataResult.Result<List<LibraryInfo>> result) {
        Gson gson = new Gson();
        Type type = new TypeToken<List<LibraryInfo>>() {
        }.getType();
        List<LibraryInfo> list = gson.fromJson(Utils.getApp().getString(R.string.library_json), type);

        result.onResult(new DataResult<>(list, new ResponseStatus()));
    }
    
    @SuppressLint("CheckResult")
    public void downloadFile(DownloadState downloadState, DataResult.Result<DownloadState> result) {
        Observable.interval(100, TimeUnit.MILLISECONDS)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(aLong -> {
                if (downloadState.isForgive || downloadState.progress == 100) {
                    return;
                }

                //模拟下载,假设下载一个文件要 10秒、每 100 毫秒下载 1% 并通知 UI 层
                if (downloadState.progress < 100) {
                    downloadState.progress = downloadState.progress + 1;
                    Log.d("---", "下载进度 " + downloadState.progress + "%");
                }

                result.onResult(new DataResult<>(downloadState, new ResponseStatus()));
                Log.d("---", "回推状态");
            });
    }
    
    private Call<String> mUserCall;
    
    public void login(User user, DataResult.Result<String> result) {
        mUserCall = retrofit.create(AccountService.class).login(user.getName(), user.getPassword());
        mUserCall.enqueue(new Callback<String>() {
            @Override
            public void onResponse(@NotNull Call<String> call, @NotNull Response<String> response) {
                ResponseStatus responseStatus = new ResponseStatus(
                    String.valueOf(response.code()), response.isSuccessful(), ResultSource.NETWORK);
                result.onResult(new DataResult<>(response.body(), responseStatus));
                mUserCall = null;
            }

            @Override
            public void onFailure(@NotNull Call<String> call, @NotNull Throwable t) {
                result.onResult(new DataResult<>(null,
                    new ResponseStatus(t.getMessage(), false, ResultSource.NETWORK)));
                mUserCall = null;
            }
        });
    }

    public void cancelLogin() {
        if (mUserCall != null && !mUserCall.isCanceled()) {
            mUserCall.cancel();
            mUserCall = null;
        }
    }

}

從這裡可以發現 Repository 的方法參數都使用 DataResult.Result

View

MainActivity

MainFragment


ViewModel

各自的 ViewViewModel 都在各自 View 中建立;都為 View內部類別 inner class


UseCase

PlayerService

public class PlayerService extends Service {

    public static final String NOTIFY_PREVIOUS = "pure_music.kunminx.previous";
    public static final String NOTIFY_CLOSE = "pure_music.kunminx.close";
    public static final String NOTIFY_PAUSE = "pure_music.kunminx.pause";
    public static final String NOTIFY_PLAY = "pure_music.kunminx.play";
    public static final String NOTIFY_NEXT = "pure_music.kunminx.next";
    private static final String GROUP_ID = "group_001";
    private static final String CHANNEL_ID = "channel_001";
    private DownloadUseCase mDownloadUseCase;


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        TestAlbum.TestMusic results = PlayerManager.getInstance().getCurrentPlayingMusic();
        if (results == null) {
            stopSelf();
            return START_NOT_STICKY;
        }

        createNotification(results);
        return START_NOT_STICKY;
    }

    private void createNotification(TestAlbum.TestMusic testMusic) {
        try {
            String title = testMusic.getTitle();
            TestAlbum album = PlayerManager.getInstance().getAlbum();
            String summary = album.getSummary();

            RemoteViews simpleContentView = new RemoteViews(
                    getApplicationContext().getPackageName(), R.layout.notify_player_small);

            RemoteViews expandedView;
            expandedView = new RemoteViews(
                    getApplicationContext().getPackageName(), R.layout.notify_player_big);

            Intent intent = new Intent(getApplicationContext(), MainActivity.class);
            intent.setAction("showPlayer");

            PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent,
                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0);

            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                NotificationManager notificationManager = (NotificationManager)
                        getSystemService(Context.NOTIFICATION_SERVICE);

                NotificationChannelGroup playGroup = new NotificationChannelGroup(GROUP_ID, getString(R.string.play));
                notificationManager.createNotificationChannelGroup(playGroup);

                NotificationChannel playChannel = new NotificationChannel(CHANNEL_ID,
                        getString(R.string.notify_of_play), NotificationManager.IMPORTANCE_DEFAULT);
                playChannel.setGroup(GROUP_ID);
                notificationManager.createNotificationChannel(playChannel);
            }

            Notification notification = new NotificationCompat.Builder(
                    getApplicationContext(), CHANNEL_ID)
                    .setSmallIcon(R.drawable.ic_player)
                    .setContentIntent(contentIntent)
                    .setOnlyAlertOnce(true)
                    .setContentTitle(title).build();

            notification.contentView = simpleContentView;
            notification.bigContentView = expandedView;

            setListeners(simpleContentView);
            setListeners(expandedView);

            notification.contentView.setViewVisibility(R.id.player_progress_bar, View.GONE);
            notification.contentView.setViewVisibility(R.id.player_next, View.VISIBLE);
            notification.contentView.setViewVisibility(R.id.player_previous, View.VISIBLE);
            notification.bigContentView.setViewVisibility(R.id.player_next, View.VISIBLE);
            notification.bigContentView.setViewVisibility(R.id.player_previous, View.VISIBLE);
            notification.bigContentView.setViewVisibility(R.id.player_progress_bar, View.GONE);

            boolean isPaused = PlayerManager.getInstance().isPaused();
            notification.contentView.setViewVisibility(R.id.player_pause, isPaused ? View.GONE : View.VISIBLE);
            notification.contentView.setViewVisibility(R.id.player_play, isPaused ? View.VISIBLE : View.GONE);
            notification.bigContentView.setViewVisibility(R.id.player_pause, isPaused ? View.GONE : View.VISIBLE);
            notification.bigContentView.setViewVisibility(R.id.player_play, isPaused ? View.VISIBLE : View.GONE);

            notification.contentView.setTextViewText(R.id.player_song_name, title);
            notification.contentView.setTextViewText(R.id.player_author_name, summary);
            notification.bigContentView.setTextViewText(R.id.player_song_name, title);
            notification.bigContentView.setTextViewText(R.id.player_author_name, summary);
            notification.flags |= Notification.FLAG_ONGOING_EVENT;

            String coverPath = Configs.COVER_PATH + File.separator + testMusic.getMusicId() + ".jpg";
            Bitmap bitmap = ImageUtils.getBitmap(coverPath);

            if (bitmap != null) {
                notification.contentView.setImageViewBitmap(R.id.player_album_art, bitmap);
                notification.bigContentView.setImageViewBitmap(R.id.player_album_art, bitmap);
            } else {
                requestAlbumCover(testMusic.getCoverImg(), testMusic.getMusicId());
                notification.contentView.setImageViewResource(R.id.player_album_art, R.drawable.bg_album_default);
                notification.bigContentView.setImageViewResource(R.id.player_album_art, R.drawable.bg_album_default);
            }

            startForeground(5, notification);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @SuppressLint("UnspecifiedImmutableFlag")
    public void setListeners(RemoteViews view) {
        int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
                ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE
                : PendingIntent.FLAG_UPDATE_CURRENT;
        try {
            PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
                    0, new Intent(NOTIFY_PREVIOUS).setPackage(getPackageName()), flags);
            view.setOnClickPendingIntent(R.id.player_previous, pendingIntent);
            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
                    0, new Intent(NOTIFY_CLOSE).setPackage(getPackageName()), flags);
            view.setOnClickPendingIntent(R.id.player_close, pendingIntent);
            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
                    0, new Intent(NOTIFY_PAUSE).setPackage(getPackageName()), flags);
            view.setOnClickPendingIntent(R.id.player_pause, pendingIntent);
            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
                    0, new Intent(NOTIFY_NEXT).setPackage(getPackageName()), flags);
            view.setOnClickPendingIntent(R.id.player_next, pendingIntent);
            pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
                    0, new Intent(NOTIFY_PLAY).setPackage(getPackageName()), flags);
            view.setOnClickPendingIntent(R.id.player_play, pendingIntent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void requestAlbumCover(String coverUrl, String musicId) {
        if (mDownloadUseCase == null) {
            mDownloadUseCase = new DownloadUseCase();
        }

        UseCaseHandler.getInstance().execute(mDownloadUseCase,
                new DownloadUseCase.RequestValues(coverUrl, musicId + ".jpg"),
                response -> startService(new Intent(getApplicationContext(), PlayerService.class)));
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }


我們可以看一下 requestAlbumCover 方法

執行 usecase

UseCaseHandler.getInstance().execute(mDownloadUseCase,

request 請求參數

new DownloadUseCase.RequestValues(coverUrl, musicId + ".jpg"),

response 回傳參數

response -> startService(new Intent(getApplicationContext(), PlayerService.class)));

非常好懂


DownloadUseCase

DownloadUseCase 繼承 UseCase
DownloadUseCase 是一個關於下載的 UseCase。
只要將下載的實作寫在 executeUseCase 方法 裡,在任何地方都能使用該功能。

public class DownloadUseCase extends UseCase<DownloadUseCase.RequestValues, DownloadUseCase.ResponseValue> {

    @Override
    protected void executeUseCase(RequestValues requestValues) {
        try {
            URL url = new URL(requestValues.url);
            InputStream is = url.openStream();
            File file = new File(Configs.COVER_PATH, requestValues.path);
            OutputStream os = new FileOutputStream(file);
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = is.read(buffer)) > 0) {
                os.write(buffer, 0, len);
            }
            is.close();
            os.close();

            getUseCaseCallback().onSuccess(new ResponseValue(file));

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static final class RequestValues implements UseCase.RequestValues {
        private String url;
        private String path;

        public RequestValues(String url, String path) {
            this.url = url;
            this.path = path;
        }

        public String getUrl() {
            return url;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public String getPath() {
            return path;
        }

        public void setPath(String path) {
            this.path = path;
        }
    }

    public static final class ResponseValue implements UseCase.ResponseValue {
        private File mFile;

        public ResponseValue(File file) {
            mFile = file;
        }

        public File getFile() {
            return mFile;
        }

        public void setFile(File file) {
            mFile = file;
        }
    }
}

總結

架構

從上述來看架構的 UML 圖可以得知,該架構框架並沒有很複雜,BaseActivity 或是 BaseFragment 做的事情也不多,base 的繼承與實作體系也不複雜,所以就不討論 base 之間的關係。

不過還是有些值得一提的,以 MainFragment 為例,這裡我們不討論 PageMessengerPlaylistAdapter,先來看一下 MainFragment 的 UML 圖。

  • ClickProxy
    負責 MainFragment 的事件方法,分別為:
    • openMenu
    • login
    • search
  • MainViewModel
    MainViewModel 為 ViewModel 負責儲存該 View 的狀態,狀態(state)變數分別為:
    • State<Boolean> initTabAndPage
    • State<String> pageAssetPath
    • State<List<TestAlbum.TestMusic>> list
  • MusicRequester
    MusicRequester 也繼承 ViewModel ,負責處理獲取音樂資源的 Request

到這裡有些人會有些疑問,有些 MVP 的 BaseFragment 會這樣編寫 BaseFragment<P extends BasePresenter> 限制繼承 base 的子類別填入 Presenter,但在這個架構框架裡不但沒有看到類似BaseFragment<VM extends BaseViewModel> 這樣的 base,還有兩個繼承 ViewModel 的子類別。

根據作者的 Comment 解釋:

基於 "單一職責原則",應將 ViewModel 劃分為
State-ViewModel 職責僅限於託管、保存和恢復本頁面 state。
Event-ViewModel 或稱 Result-ViewModel 職責僅限於 "消息分發" 場景承擔 "唯一可信源"。

以上可參考《重學安卓:這是一份 “架構模式” 自駕攻略》


職責僅限於 "消息分發" 場景承擔 "唯一可信源"的解釋:

  • 常見消息分發場景包括:數據請求,頁面間通信等,數據請求 Requester 負責,頁面通信 Messenger 負責
  • 所有事件都可交由 "唯一可信源" 在內部決策和處理,並統一分發結果給所有訂閱者頁面。

以上可參考《吃透 LiveData 本質,享用可靠消息鑑權機制》


Requester 通常按業務劃分
一個項目中通常存在多個 Requester 類,
每個頁面可根據業務需要持有多個不同 Requester 實例。
requester 職責僅限於 "業務邏輯處理" 和 "消息分發",不建議在此處理 UI 邏輯,
UI 邏輯只適合在 Activity/Fragment 等視圖控制器中完成,是 “數據驅動” 一部分,
將來升級到 Jetpack Compose 更是如此。
以上可參考《如何讓同事愛上架構模式、少寫 bug 多註釋》


因此得到的結論:

  • State-ViewModel 為 MainViewModel 同時也是 View 的內部類別並且通常與 View 成雙成對只會有1個。
  • Event-ViewModel or Result-ViewModel 為 MusicRequester(請求音樂資源的)或是未介紹的PageMessenger(SharedViewModel,可能是 Fragment 之間的通訊)。
    而 Requester 可以在 View 裡存在很多個。

題外話我不太喜歡將 MainViewModel 寫在 View 裡面,有時候 主要的ViewModel 要做的事情蠻多的,雖然我知道將 ViewModel 讓 state、event、result 分掉了,就算是這樣畢竟 View 與 ViewModel 的層還是不同。


UseCase

讓我們來看一下 Clean Architecture

根據 Uncle Bob 的 Clean Architecture 文章表示

Use Cases
The software in this layer contains application specific business rules. It encapsulates and implements all of the use cases of the system. These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their enterprise wide business rules to achieve the goals of the use case.
We do not expect changes in this layer to affect the entities. We also do not expect this layer to be affected by changes to externalities such as the database, the UI, or any of the common frameworks. This layer is isolated from such concerns.
We do, however, expect that changes to the operation of the application will affect the use-cases and therefore the software in this layer. If the details of a use-case change, then some code in this layer will certainly be affected.

Pros:

  • 業務邏輯分的很清楚。
  • 重複的Code大幅減少。
  • UseCase 彼此能互相使用,功能重用性提高。
  • UseCase 屬於領域層(Domain Layer)並非過往 Android App 架構而是獨立的一個邏輯層,因此具有獨立性。
  • 各個 UseCase 易於測試。
  • ViewModel 的 LiveData 變數大幅減少

Cons:

  • UseCase class 會越來越多。

沒玩過的 Libariy

參考文獻

別忘了按下讚並追蹤我喔~


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言